﻿namespace DataDuctus.MongoDb.Embedding.Server
{
	using System;
	using System.Diagnostics;
	using System.IO;
	using System.Runtime.InteropServices;
	using System.Threading;

	using DataDuctus.MongoDb.Embedding.Server.Internal;

	/// <summary>
	/// Extracts a mongod.exe (depending on platform architecture, x86 or x64) and runs it.
	/// </summary>
	public class EmbeddedMongoDbServer : IDisposable
	{
		#region Fields
		/// <summary>
		/// The hosting <see cref="Process"/> for mongod.exe.
		/// </summary>
		private Process process;
		#endregion // Fields

		#region Properties
		/// <summary>
		/// The binary path where mongod.exe will be executed in.
		/// </summary>
		/// <remarks>
		/// This is the working directory for mongod.exe.
		/// </remarks>
		public string BinPath { get; set; }

		/// <summary>
		/// The logfile path to store the logfile to.
		/// </summary>
		/// <remarks>
		/// The parh will be created if not existant.
		/// </remarks>
		public string LogFilePath { get; set; }

		/// <summary>
		/// If set; an explicit ip to bind the mongod.exe process to.
		/// </summary>
		public string BindIp { get; set; }

		/// <summary>
		/// The db path to store mongo database files in.
		/// </summary>
		/// <remarks>
		/// If directory do not exists it will be created.
		/// </remarks>
		public string DbPath { get; set; }

		/// <summary>
		/// Any extra arguments to pass to mongod.exe
		/// </summary>
		/// <remarks>
		/// Do not use BindIp, DbPath or LogFilePath in those.
		/// </remarks>
		public string ExtraArguments { get; set; }

		/// <summary>
		/// Time to wait after trying to nicely shutdown mongod.exe
		/// and thus <see cref="Process.Kill"/> it.
		/// </summary>
		public int ProcessEndTimeout { get; set; }

		/// <summary>
		/// If the mongod.exe stderr shall be captured using <see cref="Trace"/>.
		/// </summary>
		public bool TraceStdErr { get; set; }

		/// <summary>
		/// If the mongod.exe stdout shall be captured using <see cref="Trace"/>.
		/// </summary>
		public bool TraceStdOut { get; set; }

		/// <summary>
		/// The working directory for mongod.exe.
		/// </summary>
		/// <remarks>
		/// If not set it will be the same directory as mongod.exe.
		/// </remarks>
		public string WorkingDirectory { get; set; }
		#endregion // Properties

		#region Constructors
		/// <summary>
		/// Constructor.
		/// </summary>
		/// <remarks>
		/// The <see cref="ProcessEndTimeout"/> is set to 3s.
		/// </remarks>
		public EmbeddedMongoDbServer()
		{
			this.ProcessEndTimeout = 3000 /*3s*/;
		}
		#endregion // Constructors

		#region Public Methods
		/// <summary>
		/// Executes the mongod.exe <see cref="Process"/>.
		/// </summary>
		public void Open()
		{
			KillMongoDbProcesses(this.ProcessEndTimeout);
		  
      var serverPath = ExtractServer(this.BinPath);

			if (null == serverPath)
			{
				throw new Exception("Could not get server path (null)");
			}

			EnsureDirectories();

			// ReSharper disable AssignNullToNotNullAttribute
			process = new Process
				{
					StartInfo =
						{
							Arguments = this.RenderArguments(),
							UseShellExecute = false,
							ErrorDialog = false,
							LoadUserProfile = false,
							WindowStyle = ProcessWindowStyle.Hidden,
							CreateNoWindow = false,
							RedirectStandardError = true,
							RedirectStandardInput = false,
							RedirectStandardOutput = true,
							FileName = serverPath/*since UseShellExecute=false*/,
							WorkingDirectory = string.IsNullOrEmpty(this.WorkingDirectory) ? Path.GetDirectoryName(serverPath) : this.WorkingDirectory
						}
				};
			// ReSharper restore AssignNullToNotNullAttribute

			process.OutputDataReceived += ProcessOutputDataReceived;
			process.ErrorDataReceived += ProcessErrorDataReceived;

			process.Start();

			process.BeginOutputReadLine();
			process.BeginErrorReadLine();

			while (process.MainWindowHandle == IntPtr.Zero)
			{
				process.Refresh();
			}

			ShowWindow(process.MainWindowHandle, 0/*SW_HIDE*/);

			Thread.Sleep(1000/*1s*/); // Just to give mongo a chance to fire up...
		}
		#endregion // Public Methods

		#region Event Reception
		/// <summary>
		/// Log std err if <see cref="TraceStdErr"/> is set to true.
		/// </summary>
		/// <param name="sender">The sending <see cref="Process"/>.</param>
		/// <param name="e">The data captured from mongod.exe stderr.</param>
		void ProcessErrorDataReceived(object sender, DataReceivedEventArgs e)
		{
			if (!this.TraceStdErr)
			{
				return;
			}

			if (!string.IsNullOrEmpty(e.Data))
			{
				Trace.WriteLine(string.Format("Err - {0}",e.Data));
			}
		}

		/// <summary>
		/// Log std out if <see cref="TraceStdOut"/> is set to true.
		/// </summary>
		/// <param name="sender">The sending <see cref="Process"/>.</param>
		/// <param name="e">The data captured from mongod.exe stdout.</param>
		void ProcessOutputDataReceived(object sender, DataReceivedEventArgs e)
		{
			if (!this.TraceStdOut)
			{
				return;
			}

			if (!string.IsNullOrEmpty(e.Data))
			{
				Trace.WriteLine(string.Format("Ouput - {0}", e.Data));
			}
		}

		#endregion // Event Reception

		#region Private Helpers
    private static string ExtractServer(string binPath)
    {
      try
      {
        return ServerExtractor.ExtractServer(binPath);
      } 
      catch(IOException)
      {
        // Lets wait a second and try again before fail
        Thread.Sleep(1000/*1s*/);
        return ServerExtractor.ExtractServer(binPath);
      }
    }

		/// <summary>
		/// Creates <see cref="DbPath"/> and <see cref="LogFilePath"/> directories.
		/// </summary>
		private void EnsureDirectories()
		{
			if (!Directory.Exists(this.DbPath))
			{
				Directory.CreateDirectory(this.DbPath);
			}

			if (string.IsNullOrEmpty(this.LogFilePath))
			{
				return;
			}

			var logDir = Path.GetDirectoryName(this.LogFilePath);

			if (string.IsNullOrEmpty(logDir))
			{
				return;
			}

			if (!Directory.Exists(logDir))
			{
				Directory.CreateDirectory(logDir);
			}
		}

		/// <summary>
		/// Renders the mongod.exe arguments.
		/// </summary>
		/// <returns>An argument string.</returns>
		private string RenderArguments()
		{
			string arg = string.Format("--dbpath \"{0}\"", this.DbPath);

			if (!string.IsNullOrEmpty(this.LogFilePath))
			{
				arg += string.Format(" --logappend --logpath \"{0}\"", this.LogFilePath);
			}

			if (!string.IsNullOrEmpty(this.BindIp))
			{
				arg += string.Format(" --bind_ip {0}", this.BindIp);
			}

			if (!string.IsNullOrEmpty(this.ExtraArguments))
			{
				arg += string.Format(" {0}", this.ExtraArguments);
			}

			return arg;
		}

		/// <summary>
		/// Tries to kill *all* mongod.exe processes on current host.
		/// </summary>
		/// <param name="millisTimeout">The timeout to wait until <see cref="Process.Kill"/> is executed.</param>
		private static void KillMongoDbProcesses(int millisTimeout)
		{
			var processes = Process.GetProcessesByName("mongod");

			foreach (var proc in processes)
			{
				try
				{
					if (proc.HasExited)
					{
						continue;
					}

					ShowWindow(proc.MainWindowHandle, 5 /*SW_SHOW*/);

					if (!proc.CloseMainWindow())
					{
						proc.Kill();
					}					

					proc.WaitForExit(millisTimeout);
				}
				catch (Exception e)
				{
					Trace.TraceWarning(string.Format("Got exception when killing mongod.exe msg = {0}", e.Message));
				}
			}
		}
		#endregion // Private Helpers

		#region IDisposable
		/// <summary>
		/// <see cref="Process.Dispose()"/>es the current mongod.exe
		/// <see cref="Process"/> and frees up all resources allocated.
		/// </summary>
		/// <remarks>
		/// If the <see cref="Process.CloseMainWindow"/> do not work, it
		/// will use <see cref="Process.Dispose()"/> to destory the process.
		/// </remarks>
		public void Dispose()
		{
			if (null == this.process)
			{
				return;
			}

			try
			{
				if (!this.process.HasExited)
				{
					ShowWindow(process.MainWindowHandle, 5 /*SW_SHOW*/);

					if (!this.process.CloseMainWindow())
					{
						this.process.Kill();
					}

					this.process.WaitForExit(this.ProcessEndTimeout);
				}

				this.process.Dispose();
			}
			catch (Exception e)
			{
				Trace.TraceWarning(string.Format("Got exception when disposing the mongod server process msg = {0}", e.Message));
			}

			process = null;			
		}
		#endregion // IDisposable

		#region Dll Imports
		[DllImport("user32.dll")]
		static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
		#endregion // Dll Imports
	}
}